Есть датасет с характеристиками покупателей в магазине.
Требуется разбить покупателей на k кластеров (выбор k остается за тобой), посчитать силуэт и визуализировать полученные кластеры
Выбор k должен быть обоснован, применять можно абсолютно любые алгоритмы.
Заметка: можно генерировать новые признаки и/или не использовать все имеющиеся, да и вообще делать все, что угодно и не противоречит здравому смыслу :)
Не забывай фиксировать random_state для воспроизводимости результатов, где это требуется
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from MulticoreTSNE import MulticoreTSNE as TSNE
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
from pathlib import Path
##other libraries on need##
from sklearn.neighbors import NearestNeighbors
from random import sample
from numpy.random import uniform
import random
import plotly.graph_objs as go
import plotly as py
from scipy.cluster.hierarchy import dendrogram
from IPython.display import Image
import seaborn as sns
SEED = 42
df = pd.read_csv(Path('/Users/main/Мега_Задание_4/data/mall_customers.csv'))
print(df.shape)
df.head()
data = df.copy()
#вначалае избавимся от CustomerID, тк он равен индексу в датафрейме + 1
data = data.drop(['CustomerID'],axis = 1)
#бинаризируем пол: male = 1, female = 0
data.Gender = (data.Gender == 'Male').astype(int)
#поменяем названия столюцов Annual Income и Spending Score, чтобы к ним было удобно обращаться
data = data.rename(columns = {'Annual Income (k$)': 'Income','Spending Score (1-100)': 'SS'}, inplace = False)
data.head()
#проверяем, есть ли пустые ячейки - всё, обработали датафрейм
data.isnull().sum()
#проверим, существует ли кластерная структура - статистика Хопкинса
def hop_stat(X):
n, d = X.shape[0], X.shape[1]
m = int(0.1 * n)
nbrs = NearestNeighbors(n_neighbors=1).fit(X)
random.seed(SEED)
rand_X = sample(range(n), m)
#print(rand_X)
u_i = []
w_i = []
for j in range(m):
random.seed(SEED)
u_dist = nbrs.kneighbors((uniform(np.amin(X,axis=0),np.amax(X,axis=0))).reshape(1, -1), 2, return_distance=True)[0]
u_i.append(u_dist[0][1])
w_dist = nbrs.kneighbors(X[rand_X[j]:rand_X[j] + 1].to_numpy().reshape(1, -1), 2, return_distance=True)[0]
w_i.append(w_dist[0][1])
H = sum(w_i) / (sum(u_i) + sum(w_i))
return H
print('Hopkins statistics =',hop_stat(data))
Кластерная структура существует. Запишем также функцию поика инерции.
def inertia(X):
inertia = []
for k in range(1, 12):
kmeans = KMeans(n_clusters=k, random_state=1).fit(X)
inertia.append(kmeans.inertia_)
fig = plt.figure(figsize=(14, 6))
plt.plot(range(1, 12), inertia, marker='s');
fig.set(facecolor = 'white')
plt.title('Inertia', fontsize=13)
plt.xlabel('$k$')
plt.ylabel('$\Phi_0$');
return
Кластерная структура существует
Уменьшим размерность с помошью t-SNE до 2-D и попробуем построить все 3 типа кластеризации с этими данными.
%%time
data_reduc = TSNE(n_components=2, random_state=SEED, metric='euclidean', n_jobs=-1).fit_transform(data)
fig = plt.figure(figsize=(8, 8))
fig.set(facecolor = 'white')
plt.scatter(data_reduc[:, 0], data_reduc[:, 1], c='g', s=20, cmap=plt.cm.get_cmap('nipy_spectral', 10))
plt.title('t-SNE projection', fontsize=13);
Получили вот такие данные. Попробуем кластеризовать с помощью K-means.
inertia(data_reduc)
По графику видно, что возможные точки K для деления - это 4, 5, 6. Рассмотрим их все и посчитаем коэффициент силуэта. Для достоверности посмотрим K = 7, чтобы увидеть, что силуэт для него меньше, чем для предыдущего.
def fit_kmeans(X,K):
kmeans = KMeans(n_clusters = K, random_state = SEED).fit(X)
yhat_kmeans = kmeans.predict(X)
#print('Для K =',K,'Silhouette score =',silhouette_score(X, yhat_kmeans))
return silhouette_score(X, yhat_kmeans)
def plot_kmeans(X, K):
kmeans = KMeans(n_clusters = K, random_state = SEED).fit(X)
yhat_kmeans = kmeans.predict(X)
#print('Для K =',K,'Silhouette score =',silhouette_score(X, yhat_kmeans))
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' for x in yhat_kmeans]
fig = plt.figure(figsize=(8,8))
fig.set(facecolor = 'white')
for i in range(kmeans.cluster_centers_.shape[0]):
plt.plot(kmeans.cluster_centers_[i, 0], kmeans.cluster_centers_[i, 1], 'rX')
title = 'Kmeans для K = '+str(K)+' имеет Silhouette = '+str(round(silhouette_score(X, yhat_kmeans),5))
plt.title(title, fontsize=13)
plt.scatter(X[:,0], X[:,1], c=colors, picker=True);
return silhouette_score(X, yhat_kmeans)
maximum, K = -1, 0
for i in range(5,8):
print('Для K =',i,'Silhouette score =',fit_kmeans(data_reduc,i))
if fit_kmeans(data_reduc,i) > maximum:
maximum = fit_kmeans(data_reduc,i)
K = i
print('Best: Для K =',K,'Silhouette score = ',plot_kmeans(data_reduc,K))
Видим, что максимальный силуэт получаем в случае K = 6 и он равен Silhouette score = 0.660432119105476. Теперь попробуем dbscan.
def dbs(X,e,n):
dbscan = DBSCAN(eps = e, min_samples = n).fit(X)
yhat_dbscan = dbscan.labels_
return silhouette_score(X, yhat_dbscan)
maximum, eps, min_samples = -1, 0., 0
for i in range(2,11):
for j in np.arange(0.9,10.1,0.1):
#print(dbs(data_reduc,j,i))
if dbs(data_reduc,j,i) > maximum:
maximum = dbs(data_reduc,j,i)
eps = j
min_samples = i
print('Best SilScore for dbscan =',maximum,'when eps =',eps,', min_samples =',min_samples)
Построим график с наилучшеми параметрами.
dbscan = DBSCAN(eps=1.9, min_samples=2, n_jobs=-1).fit(data_reduc)
yhat_dbscan = dbscan.labels_
#plot
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' for x in yhat_dbscan]
fig = plt.figure(figsize=(8,8))
plt.title('DBSCAN', fontsize=13)
fig.set(facecolor = 'white')
plt.scatter(data_reduc[:,0], data_reduc[:,1], c=colors, picker=True);
Как мы видим, разбиение, конечно, хорошее с точки зрения коэффициента силуэта, но в большой верхней группе наблюдаются подгруппы, которые DBSCAN не выделил.
def plot_dendrogram(model, **kwargs):
counts = np.zeros(model.children_.shape[0])
n_samples = len(model.labels_)
for i, merge in enumerate(model.children_):
current_count = 0
for child_idx in merge:
if child_idx < n_samples:
current_count += 1
else:
current_count += counts[child_idx - n_samples]
counts[i] = current_count
linkage_matrix = np.column_stack([model.children_, model.distances_, counts]).astype(float)
# Plot the corresponding dendrogram
dendrogram(linkage_matrix, **kwargs)
return
agclust = AgglomerativeClustering(n_clusters=None, distance_threshold=1).fit(data_reduc)
fig = plt.figure(figsize=(7, 7))
fig.set(facecolor = 'white')
plot_dendrogram(agclust)
def fit_agclust(data_reduc,num_clus):
agclust = AgglomerativeClustering(n_clusters=num_clus, distance_threshold=None).fit(data_reduc)
yhat_agclust = agclust.labels_
return silhouette_score(data_reduc, yhat_agclust)
maximum, i = -1, 0
for i in range(4,8):
if fit_agclust(data_reduc,i) > maximum:
maximum = fit_agclust(data_reduc,i)
cnt = i
print('Best: Для num_clus =',cnt,'Silhouette score =',maximum)
def plot_agclust(data_reduc,num_clus):
agclust = AgglomerativeClustering(n_clusters=num_clus, distance_threshold=None).fit(data_reduc)
yhat_agclust = agclust.labels_
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' if x ==5
else 'gray' for x in yhat_agclust]
fig = plt.figure(figsize=(8,8))
fig.set(facecolor = 'white')
plt.title('Агломеративная иерархическая кластеризация')
plt.scatter(data_reduc[:,0], data_reduc[:,1], c=colors, picker=True);
print('Для num_clus =',num_clus,'Silhouette score =',silhouette_score(data_reduc, yhat_agclust))
return
plot_agclust(data_reduc,6)
На примере аглмеративной кластеризации можно посмотреть, что за кластеры у нас получились.
agclust = AgglomerativeClustering(n_clusters=6, distance_threshold=None).fit(data_reduc)
yhat_agclust = agclust.labels_
X = pd.Series(data_reduc[:,0])
Y = pd.Series(data_reduc[:,1])
data['X'] = X
data['Y'] = Y
data['yhat_agclust'] = pd.Series(yhat_agclust)
X_n = []
Y_n = []
for i in range(6):
print('')
print('В кластере',i,':')
X_n.append(data[data.yhat_agclust == i]['X'].mean())
Y_n.append(data[data.yhat_agclust == i]['Y'].mean())
for j in data.columns:
print('Cредний',j,'равен',data[data.yhat_agclust == i][j].mean())
По средним координатам, можно понять, какой номер у каждого из нарисованных кластеров. Таким образом можно интерпретировать кластеры даже в случае понижения размерности.
agclust = AgglomerativeClustering(n_clusters=6, distance_threshold=None).fit(data_reduc)
yhat_agclust = agclust.labels_
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' if x ==5
else 'gray' for x in yhat_agclust]
fig = plt.figure(figsize=(8,8))
fig.set(facecolor = 'white')
plt.title('Агломеративная кластеризация + идентификация кластеров')
for i in range(6):
title = 'Кластер ' + str(i)
plt.annotate(title, xy = (X_n[i], Y_n[i]), fontsize = 14, weight = 'bold')
plt.scatter(data_reduc[:,0], data_reduc[:,1], c=colors, picker=True);
Рассмотрим, какие коэффициенты силуэта мы получили для данных при уменьшении размерности.
print('Best: K-means при K = 6 Silhouette score =', fit_kmeans(data_reduc,6))
print('Best: DBSCAN при eps = 1.9 и min_samples = 2 Silhouette score =', dbs(data_reduc, 1.9, 2))
print('Best: Агломеративная иерархическая кластеризация при num_clasters = 6 Silhouette score =', fit_agclust(data_reduc,6))
Лучший резльтат у DBSCAN, но мы помним, что разбил он всего на 2 кластера, что говорит нам, что всё-таки K-means лучше показал себя c точки зрения деления на более мелкие класетры.
Рассмотрим, как ведут себя данные, если мы разделим их по полу.
data = data.drop(['X','Y','yhat_agclust'],axis = 1)
sns.set_style("dark")
pic = sns.pairplot(data, hue='Gender',palette = {0:'goldenrod',1:'royalblue'}, aspect=1.5)
pic.fig.suptitle('Data depending on gender',y = 1.05)
plt.show()
Из графиков выше видно, что пол не сильно влияет на данные. Поэтому этот столбец можно выкинуть и посмотреть 3-D кластеризацию.
data_gender = data.drop(['Gender'], axis=1)
inertia(data_gender)
for K in range(5,8):
print('Для K =',K,'Silhouette score =',fit_kmeans(data_gender, K))
algorithm = KMeans(n_clusters = 6 ,init='k-means++', n_init = 10 ,max_iter=300,
tol=0.0001, random_state= SEED , algorithm='elkan')
algorithm.fit(data_gender)
labels = algorithm.labels_
print('Silhouette score',silhouette_score(data_gender, labels))
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' if x ==5
else 'gray' for x in labels]
sex_colors = ['royalblue' if x == 1 else 'goldenrod' for x in data.Gender]
data_gender['labels'] = labels
trace1 = go.Scatter3d(x = data_gender.Age, y = data_gender.SS, z = data_gender.Income, mode='markers',
marker=dict(color = sex_colors, size= 7,line=dict(color= colors,width= 13),opacity=0.9))
dat = [trace1]
layout = go.Layout(title = 'K-means 3-D',scene = dict(xaxis = dict(title = 'Age'), yaxis = dict(title = 'Spending Score'),
zaxis = dict(title = 'Annual Income')))
fig = go.Figure(data=dat, layout=layout)
py.offline.iplot(fig)
Получили вот такую 3-D кластеризаицию с помощью K-means (синий центр - мужчины, желтый - женщины). Однако, самый лучший коэффициент силуэта ниже, чем был при уменьшении размерности.
agclust3d = AgglomerativeClustering(n_clusters=None, distance_threshold=1).fit(data_gender)
fig = plt.figure(figsize=(7, 7))
plt.title('Дендограмма',fontsize = 12)
fig.set(facecolor = 'white')
plot_dendrogram(agclust3d)
maximum, i = -1, 0
for i in range(4,8):
if fit_agclust(data_gender,i) > maximum:
maximum = fit_agclust(data_gender,i)
cnt = i
print('Best: Для num_clus =',cnt,'Silhouette score =',maximum)
algorithm = AgglomerativeClustering(n_clusters = 6, distance_threshold=None)
algorithm.fit(data_gender)
labels = algorithm.labels_
colors = ['darkkhaki' if x==0 else 'indianred' if x==1 else 'seagreen'
if x==2 else 'darkgoldenrod' if x ==3 else 'cornflowerblue' if x == 4 else 'plum' if x ==5
else 'gray' for x in labels]
sex_colors = ['royalblue' if x == 1 else 'goldenrod' for x in data.Gender]
print('Silhouette score',silhouette_score(data_gender, labels))
data_gender['labels'] = labels
trace1 = go.Scatter3d(x = data_gender.Age, y = data_gender.SS, z = data_gender.Income, mode='markers',
marker=dict(color = sex_colors, size= 7,line=dict(color = colors,width= 13),opacity=0.9))
dat = [trace1]
layout = go.Layout(title = 'Агломеративная кластеризация 3-D',scene = dict(xaxis = dict(title = 'Age'), yaxis = dict(title = 'Spending Score'),
zaxis = dict(title = 'Annual Income')))
fig = go.Figure(data=dat, layout=layout)
py.offline.iplot(fig)
На 3-D графиках точки с синим центром - мужчины, а точки с желтым - женщины. Несмотря на низкий коэффициент силуэта, при 3-D кластеризации намного проще идентифицировать кластеры.
for i in range(6):
print('В группе',i,':')
for j in data_gender.columns:
print('Cредний',j,'равен',data_gender[data_gender['labels'] == i][j].mean())
Выведем лучшие коэффициенты силуэта для 3-D кластеризации.
data_gender = data_gender.drop(['labels'],axis = 1)
print('Best: K-means 3-D при K = 6 Silhouette score =', fit_kmeans(data_gender,6))
print('Best: Агломеративная иерархическая кластеризация 3-D при num_clasters = 6 Silhouette score =', fit_agclust(data_gender,6))
Возьмем стандартный датасет ирисов, состоящий из 4-х признаков длины/ширины внутренней и наружной долей околоцветника и таргета - вид цветка (kind).
1.Требуется кластеризовать цветки (да-да, количество кластеров уже известно), посчитать accuracy. Выбор алгоритма опять же на усмотрение (может быть несколько)
Важно: метки могут расставиться в другом порядке относительно истинного таргета, то есть после кластеризации будут получены 0, а это на самом деле 1, то есть надо сделать отображение $0 \rightarrow 1$ , то же самое касается и других меток.
Поэтому, получив метки кластеров, стоит перебрать все возможные их перестановки. В этом поможет itertools.permutations
2.Воспользоваться любым/любыми пройденными/известными алгоритмами классификации и посчитать accuracy
(Например, логистической регрессией)
from sklearn import datasets
import itertools
##other libraries on need##
from sklearn.metrics import accuracy_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
df = datasets.load_iris()
df = pd.DataFrame(np.hstack([df['data'], df['target'].reshape(-1,1)])
, columns=['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'kind'])
print('Count of unique flowers', df['kind'].unique().shape[0])
df.head()
df.tail()
def validate_with_mappings(y_true, y_pred):
'''
Check all possible permutations to maximize accuracy
'''
l = []
permutations = itertools.permutations([0, 1, 2])
for a, b, c in permutations:
mapping = {0 : a, 1: b, 2: c}
mapped_preds = [mapping[pred] for pred in y_pred]
l.append((mapping, sum(mapped_preds == y_true) / len(y_true)))
return l
X = df.iloc[:, :-1]
y = df['kind']
##твой код с выбором алгоритма ##
kmeans = KMeans(n_clusters = 3, random_state = SEED).fit(X)
y_pred = kmeans.predict(X)
#print('y_pred = ',y_pred)
#print('y_true = ',y.to_numpy())
maximum, ind = 0, 0
for i in range(len(validate_with_mappings(y, y_pred))):
if validate_with_mappings(y, y_pred)[i][1] > maximum:
maximum = validate_with_mappings(y, y_pred)[i][1]
ind = i
print(validate_with_mappings(y, y_pred)[ind])
dic = validate_with_mappings(y, y_pred)[ind][0]
y_pred_pd = pd.Series(y_pred)
y_pred_pd = y_pred_pd.apply(lambda x: dic[0] if x ==0 else dic[1] if x == 1 else dic[2])
#print(y_pred_pd)
print('K-means Accuracy score =',accuracy_score(y,y_pred_pd))
##твой код##
#сплитуем на test и train, потому что применять предсказания к выборке, на которой обучаслись = переобученная модель
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = SEED)
LR = LogisticRegression(penalty='l2',random_state = SEED,max_iter=10000)
LR.fit(X_train,y_train)
y_pred = LR.predict(X_test)
#print('y_true =',y_test.to_numpy())
#print('y_pred =',y_pred)
print('LogReg Accuracy score = ',accuracy_score(y_test,y_pred)) #такой хороший accuracy только из-за маленького датафрейма (на мой взгляд)
print('Классификация лучше кластеризации на',round((accuracy_score(y_test,y_pred) - accuracy_score(y,y_pred_pd))*100,2),'%')
Вопрос: что оказалось лучше: алгоритм классификации или кластеризации и на сколько?
Ответ: **
Классификация лучше и это логично, потому что кластеризация нужна для разделения данных на кластеры, когда этих кластеров заранее нет. А вот классификация как раз используется для разделения данных на уже существующие классы. В данном случае имеем как раз вторую ситуацию, однако при большем размере датафрейма классификация могла сработать не так хорошо.